/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.catalina.startup;

import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.apache.catalina.deploy.*;
import org.apache.catalina.util.Introspection;
import org.apache.tomcat.util.res.StringManager;

import javax.annotation.Resource;
import javax.annotation.Resources;
import javax.annotation.security.DeclareRoles;
import javax.annotation.security.RunAs;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
 * <p><strong>AnnotationSet</strong> for processing the annotations of the web application
 * classes (<code>/WEB-INF/classes</code> and <code>/WEB-INF/lib</code>).</p>
 *
 * @author Fabien Carrion
 */
public class WebAnnotationSet {

	/**
	 * The string resources for this package.
	 */
	protected static final StringManager sm =
			StringManager.getManager(Constants.Package);
	private static final String SEPARATOR = "/";


	// --------------------------------------------------------- Public Methods

	/**
	 * Process the annotations on a context.
	 */
	public static void loadApplicationAnnotations(Context context) {

		loadApplicationListenerAnnotations(context);
		loadApplicationFilterAnnotations(context);
		loadApplicationServletAnnotations(context);


	}


	// -------------------------------------------------------- protected Methods

	/**
	 * Process the annotations for the listeners.
	 */
	protected static void loadApplicationListenerAnnotations(Context context) {
		Class<?> classClass = null;
		String[] applicationListeners =
				context.findApplicationListeners();
		for (int i = 0; i < applicationListeners.length; i++) {
			classClass = Introspection.loadClass(context,
					applicationListeners[i]);
			if (classClass == null) {
				continue;
			}

			loadClassAnnotation(context, classClass);
			loadFieldsAnnotation(context, classClass);
			loadMethodsAnnotation(context, classClass);
		}
	}

	/**
	 * Process the annotations for the filters.
	 */
	protected static void loadApplicationFilterAnnotations(Context context) {
		Class<?> classClass = null;
		FilterDef[] filterDefs = context.findFilterDefs();
		for (int i = 0; i < filterDefs.length; i++) {
			classClass = Introspection.loadClass(context,
					(filterDefs[i]).getFilterClass());
			if (classClass == null) {
				continue;
			}

			loadClassAnnotation(context, classClass);
			loadFieldsAnnotation(context, classClass);
			loadMethodsAnnotation(context, classClass);
		}
	}

	/**
	 * Process the annotations for the servlets.
	 */
	protected static void loadApplicationServletAnnotations(Context context) {

		Wrapper wrapper = null;
		Class<?> classClass = null;

		Container[] children = context.findChildren();
		for (int i = 0; i < children.length; i++) {
			if (children[i] instanceof Wrapper) {

				wrapper = (Wrapper) children[i];
				if (wrapper.getServletClass() == null) {
					continue;
				}

				classClass = Introspection.loadClass(context,
						wrapper.getServletClass());
				if (classClass == null) {
					continue;
				}

				loadClassAnnotation(context, classClass);
				loadFieldsAnnotation(context, classClass);
				loadMethodsAnnotation(context, classClass);

                /* Process RunAs annotation which can be only on servlets.
                 * Ref JSR 250, equivalent to the run-as element in
                 * the deployment descriptor
                 */
				RunAs annotation = classClass.getAnnotation(RunAs.class);
				if (annotation != null) {
					wrapper.setRunAs(annotation.value());
				}
			}
		}


	}

	/**
	 * Process the annotations on a context for a given className.
	 */
	protected static void loadClassAnnotation(Context context,
	                                          Class<?> classClass) {
	    /* Process Resource annotation.
         * Ref JSR 250
         */
		{
			Resource annotation = classClass.getAnnotation(Resource.class);
			if (annotation != null) {
				addResource(context, annotation);
			}
		}
        /* Process Resources annotation.
         * Ref JSR 250
         */
		{
			Resources annotation = classClass.getAnnotation(Resources.class);
			if (annotation != null && annotation.value() != null) {
				for (Resource resource : annotation.value()) {
					addResource(context, resource);
				}
			}
		}
        /* Process EJB annotation.
         * Ref JSR 224, equivalent to the ejb-ref or ejb-local-ref
         * element in the deployment descriptor.
        {
            EJB annotation = classClass.getAnnotation(EJB.class);
            if (annotation != null) {

                if ((annotation.mappedName().length() == 0)
                        || annotation.mappedName().equals("Local")) {

                    ContextLocalEjb ejb = new ContextLocalEjb();

                    ejb.setName(annotation.name());
                    ejb.setType(annotation.beanInterface().getCanonicalName());
                    ejb.setDescription(annotation.description());

                    ejb.setHome(annotation.beanName());

                    context.getNamingResources().addLocalEjb(ejb);

                } else if (annotation.mappedName().equals("Remote")) {

                    ContextEjb ejb = new ContextEjb();

                    ejb.setName(annotation.name());
                    ejb.setType(annotation.beanInterface().getCanonicalName());
                    ejb.setDescription(annotation.description());

                    ejb.setHome(annotation.beanName());

                    context.getNamingResources().addEjb(ejb);

                }
            }
        }
        */
        /* Process WebServiceRef annotation.
         * Ref JSR 224, equivalent to the service-ref element in 
         * the deployment descriptor.
         * The service-ref registration is not implemented
        {
            WebServiceRef annotation = classClass
                    .getAnnotation(WebServiceRef.class);
            if (annotation != null) {
                ContextService service = new ContextService();

                service.setName(annotation.name());
                service.setWsdlfile(annotation.wsdlLocation());

                service.setType(annotation.type().getCanonicalName());

                if (annotation.value() == null)
                    service.setServiceinterface(annotation.type()
                            .getCanonicalName());

                if (annotation.type().getCanonicalName().equals("Service"))
                    service.setServiceinterface(annotation.type()
                            .getCanonicalName());

                if (annotation.value().getCanonicalName().equals("Endpoint"))
                    service.setServiceendpoint(annotation.type()
                            .getCanonicalName());

                service.setPortlink(annotation.type().getCanonicalName());

                context.getNamingResources().addService(service);
            }
        }
        */
        /* Process DeclareRoles annotation.
         * Ref JSR 250, equivalent to the security-role element in
         * the deployment descriptor
         */
		{
			DeclareRoles annotation = classClass
					.getAnnotation(DeclareRoles.class);
			if (annotation != null && annotation.value() != null) {
				for (String role : annotation.value()) {
					context.addSecurityRole(role);
				}
			}
		}
	}

	protected static void loadFieldsAnnotation(Context context,
	                                           Class<?> classClass) {
		// Initialize the annotations
		Field[] fields = Introspection.getDeclaredFields(classClass);
		if (fields != null && fields.length > 0) {
			for (Field field : fields) {
				Resource annotation = field.getAnnotation(Resource.class);
				if (annotation != null) {
					String defaultName = classClass.getName() + SEPARATOR + field.getName();
					Class<?> defaultType = field.getType();
					addResource(context, annotation, defaultName, defaultType);
				}
			}
		}
	}

	protected static void loadMethodsAnnotation(Context context,
	                                            Class<?> classClass) {
		// Initialize the annotations
		Method[] methods = Introspection.getDeclaredMethods(classClass);
		if (methods != null && methods.length > 0) {
			for (Method method : methods) {
				Resource annotation = method.getAnnotation(Resource.class);
				if (annotation != null) {
					if (!Introspection.isValidSetter(method)) {
						throw new IllegalArgumentException(sm.getString(
								"webAnnotationSet.invalidInjection"));
					}

					String defaultName = classClass.getName() + SEPARATOR +
							Introspection.getPropertyName(method);

					Class<?> defaultType =
							(method.getParameterTypes()[0]);
					addResource(context, annotation, defaultName, defaultType);
				}
			}
		}
	}

	/**
	 * Process a Resource annotation to set up a Resource.
	 * Ref JSR 250, equivalent to the resource-ref,
	 * message-destination-ref, env-ref, resource-env-ref
	 * or service-ref element in the deployment descriptor.
	 */
	protected static void addResource(Context context, Resource annotation) {
		addResource(context, annotation, null, null);
	}

	protected static void addResource(Context context, Resource annotation,
	                                  String defaultName, Class<?> defaultType) {
		String name = getName(annotation, defaultName);
		String type = getType(annotation, defaultType);

		if (type.equals("java.lang.String") ||
				type.equals("java.lang.Character") ||
				type.equals("java.lang.Integer") ||
				type.equals("java.lang.Boolean") ||
				type.equals("java.lang.Double") ||
				type.equals("java.lang.Byte") ||
				type.equals("java.lang.Short") ||
				type.equals("java.lang.Long") ||
				type.equals("java.lang.Float")) {

			// env-ref element
			ContextEnvironment resource = new ContextEnvironment();

			resource.setName(name);
			resource.setType(type);

			resource.setDescription(annotation.description());

			resource.setValue(annotation.mappedName());

			context.getNamingResources().addEnvironment(resource);

		} else if (type.equals("javax.xml.rpc.Service")) {

			// service-ref element
			ContextService service = new ContextService();

			service.setName(name);
			service.setWsdlfile(annotation.mappedName());

			service.setType(type);
			service.setDescription(annotation.description());

			context.getNamingResources().addService(service);

		} else if (type.equals("javax.sql.DataSource") ||
				type.equals("javax.jms.ConnectionFactory") ||
				type.equals("javax.jms.QueueConnectionFactory") ||
				type.equals("javax.jms.TopicConnectionFactory") ||
				type.equals("javax.mail.Session") ||
				type.equals("java.net.URL") ||
				type.equals("javax.resource.cci.ConnectionFactory") ||
				type.equals("org.omg.CORBA_2_3.ORB") ||
				type.endsWith("ConnectionFactory")) {

			// resource-ref element
			ContextResource resource = new ContextResource();

			resource.setName(name);
			resource.setType(type);

			if (annotation.authenticationType()
					== Resource.AuthenticationType.CONTAINER) {
				resource.setAuth("Container");
			} else if (annotation.authenticationType()
					== Resource.AuthenticationType.APPLICATION) {
				resource.setAuth("Application");
			}

			resource.setScope(annotation.shareable() ? "Shareable" : "Unshareable");
			resource.setProperty("mappedName", annotation.mappedName());
			resource.setDescription(annotation.description());

			context.getNamingResources().addResource(resource);

		} else if (type.equals("javax.jms.Queue") ||
				type.equals("javax.jms.Topic")) {

			// message-destination-ref
			MessageDestinationRef resource = new MessageDestinationRef();

			resource.setName(name);
			resource.setType(type);

			resource.setUsage(annotation.mappedName());
			resource.setDescription(annotation.description());

			context.getNamingResources().addMessageDestinationRef(resource);

		} else if (type.equals("javax.resource.cci.InteractionSpec") ||
				type.equals("javax.transaction.UserTransaction") ||
				true) {

			// resource-env-ref
			ContextResourceEnvRef resource = new ContextResourceEnvRef();

			resource.setName(name);
			resource.setType(type);

			resource.setProperty("mappedName", annotation.mappedName());
			resource.setDescription(annotation.description());

			context.getNamingResources().addResourceEnvRef(resource);

		}
	}

	private static String getType(Resource annotation, Class<?> defaultType) {
		Class<?> type = annotation.type();
		if (type == null || type.equals(Object.class)) {
			if (defaultType != null) {
				type = defaultType;
			}
		}
		return Introspection.convertPrimitiveType(type).getCanonicalName();
	}

	private static String getName(Resource annotation, String defaultName) {
		String name = annotation.name();
		if (name == null || name.equals("")) {
			if (defaultName != null) {
				name = defaultName;
			}
		}
		return name;
	}
}
